/*
	Script: section.js
		Foo - Framework for Rapid RIA Development

	License:
		MIT-style license.

	Foo Copyright:
		Copyright (c) 2007 Rizqi Ahmad, <http://riiv.net>

	Foo Credits:
		- Uses prototype.js <http://prototype.conio.net/> (c) 2005 Sam Stephenson sam [at] conio [dot] net, MIT-style license
		- Documentation by Rizqi Ahmad (raid_ox [at] yahoo [dot] de).
*/

/*
	Class: foo.Section
		Foo Section Class
	
	Properties:
		element - HTML Element
		id - Section id, section can be accessed globally using this id
		parent - Parent section
		children - Children sections
		events - Section events
		clients - Sections/Datasets that depend on this section
		servers - Sections/Datasets binded to this section
		queue - execution queue, executed after rendering
		storage - Section storage system
		elementData - Data register that can be accessed through DOM object (element.getData(dataName))
		sectionContents - Section contents
		modules - Implemented modules
		config - configurations
		tokens - Tokens
		unique - Unique counter
		datasources - Datasources
		bindings - Bindings
*/
foo.Section = Class.create({
/*
	Function: initialize
		Initialize section
	
	Parameters:
		node - Element node
		content - optional, html content
*/
	initialize: function(node, options){
		this.element = $(node);
		this.element.addClassName(foo.sectionClassName);
		this.id = this.element.id;
		window[this.id] = this;
		
		
		this.parent = this.element.getSection();
		this.children = [];
		if(this.parent)
			this.parent.children.push(this);
		
		this.element.section = this;
		
		this.events = {
			initialize: [],
			loading: [],
			loaded: [],
			update: [],
			refresh: [],
			destroy: []
		};
		
		if(options){
			if(options.content)
				this.element.innerHTML = options.content;
			if(options.templateText)
				this.templateText = options.templateText;
			if(options.template)
				this.template = options.template;
			if(options.events)
				for(var i in options.events){
					if(this.events[i])
						this.events[i] = this.events[i].concat(options.events[i]);
					else
						this.events[i] = options.events[i].clone();
				}
		}
		
		this.clients = [];
		this.servers = [];
		this.queue = [];
		
		this.storage = {};
		this.elementData = {};
		
		this.modules = [];
		this.config = {};
		this.statements = {};
		this.tokens = {};
		
		this.unique = 0;
		
		this.datasources = false;
		this.bindings = false;
		
		foo.baseModules.each(this.implement.bind(this));
		
		this.fireEvent('initialize');
		foo.fireEvent('sectionInit', this);
		
		this.element.hide();
		this.compile();
		this.element.show();
		
		this.refresh();
	},

/*
	Function: loading
		Set state to be "loading" and fire "loading" event
*/
	loading: function(){
		this.fireEvent('loading');
	},

/*
	Function: loaded
		Set state to be "loaded" and fire "loaded" event
*/
	loaded: function(){
		this.fireEvent('loaded');
	},

/*
	Function: update
		Update section and binded sections/datasets, does not render screens
*/
	update: function(){
		this.fireEvent('update');
		foo.fireEvent('sectionUpdate', this);
		this.clients.invoke('refresh', this);
	},

/*
	Function: refresh
		Update section and binded sections/datasets and rerender screens
*/
	refresh: function(){
		this.fireEvent('update');
		foo.fireEvent('sectionUpdate', this);
		
		if(this.datasource && !this.datasource.ready)
			return;
		if(this.bindings && this.bindings.length != this.bindingNames.length)
			return;
		
		this.render();
		
		this.children.invoke('destroy');
		this.children.clear();
		
		this.queue.invoke('apply', this);
		this.queue.clear();
		
		this.fireEvent('refresh');
		foo.fireEvent('sectionRefresh', this);
		this.clients.invoke('refresh', this);
	},

/*
	Function: implement
		Implement module to section
	
	Parameters:
		module - Module to be implemented
*/	
	implement: function(module){
		if(typeof module == 'string'){
			module = foo.modules[module];
			if(!module)
				return;
		}
		if(module.requires)
			module.requires.each(this.implement.bind(this));
		
		Object.extend(this.tokens, module.tokens);
		Object.extend(this.statements, module.statements);
		
		if(module.events)
			module.events.each(function(e){
				if(!this.events[e])
					this.events[e] = [];
			}, this);
		
		for(var i in this.events)
			if(module.listeners[i])
				this.events[i].push(module.listeners[i]);
		
		for(var i in module.globals){
			var descendant = module.globals[i], ancestor = this[i];
			if(ancestor && Object.isFunction(ancestor) && descendant.argumentNames().first() == '$super'){
				var method = descendant, descendant = ancestor.wrap(method);
				Object.extend(descendant, {
				valueOf:  function() { return method },
				toString: function() { return method.toString() }
			  });
			};
			this[i] = descendant;
		}
		
		module.initialize.apply(this);
		this.modules.push(module);
		this.parseTokens();
	},

/*
	Function: render
		Render screens using template functions that are generated from markups
*/
	render: function(){
		delete this.storage;
		delete this.elementData;
		this.storage = {};
		this.elementData = {};
		this.unique = 0;
		
		this.element.innerHTML = this.template();
	},

/*
	Function: compile
		Compile markups to template functions
*/
	compile: function(){
		if(this.template)
			return;
		this.templateSource = this.element.innerHTML;
		this.templateText = this.parse();
		try {
			this.template = new Function('$data', foo.closure[0] + this.templateText + foo.closure[1]);
			this.element.innerHTML = '';
		} catch(e){
			this.error = e;
			this.template = function(){foo.Console.log('Error', 'Cannot execute emplate "'+this.id+'"')};
			this.element.innerHTML = 'Error encountered!';
		}
	},

/*
	Function: parse
		Parse markups to javascript
	
	Parameters:
		node - Starting point elmenet
		options - options
*/
	parse: function(node, options){
		var options = options || {};
		var statement, value, attr;
		var result = [];
		
		
		if(!node){
			node = this.element;
			options.excludeNode = true;
		}
		
		var nodeName = node.nodeName.toLowerCase();
		var statements = {};
		Object.extend(statements, this.statements);
		Object.extend(statements, this.tokens);
		
		
		if(nodeName == '#text' && (/[^ \t\n]+/).test(node.nodeValue)){
			result.push(foo.parseText(node.nodeValue));
		}else if(node.nodeName.toLowerCase() != 'script' && node.nodeType == 1){
			node = $(node);
			
			attr = $A(node.attributes);
			if(foo.reversion)
				attr = attr.reverse();
				
			for(var i=0; i<attr.length; i++){
				for(var state in statements){
					if(attr[i].nodeName.toLowerCase() == foo.ns+':'+state){
						statement = state;
						value = attr[i].nodeValue;
						break;
					}
				}
				if(statement)
					break;
			}
			
			// If it is a section
			if(node != this.element && (node.hasClassName(foo.sectionClassName) || this.tokens[statement])){
				var clone = node.cloneNode(false);
				var section = new foo.Section(node);
				
				var sectionTemplateID = foo.sectionTemplates.length;
				foo.sectionTemplates.push({content:section.templateSource, template: section.template, events: section.events});
				
				section.destroy();
				delete section;
				
				this.addFuid(node);
				var id = node.id;
				
				var variable = '_sectionID' + foo.idx++;
				var res = [
					foo.parseText(foo.Utils.serializeTag(clone)),
					foo.parseText(foo.Utils.serializeTag(clone, true)),
					"var "+variable+" = "+(node.hasClassName('fuid') ? "this.getLastFuid()" : foo.parseInline(node.id))+";",
					"this.addQueue(new Function('new foo.Section('+"+variable+".toJSON()+', foo.sectionTemplates["+sectionTemplateID+"])'));"
				].join('\n');
				result.push(res);
				
				
			}else if(statement){
				node.removeAttribute(foo.ns+':'+statement);
				result.push(this.statements[statement].apply(this, [node, value, options]));
			}else{
				if(!options.excludeNode)
					result.push(foo.parseText(foo.Utils.serializeTag(node)));
				
				if(node.nodeName.toLowerCase() == 'textarea')
					result.push(foo.parseText(node.value));
				else if(!options.noChild)
					for(var i=0; i<node.childNodes.length; i++)
						result.push(this.parse(node.childNodes[i]));
				
				if(!options.excludeNode)
					result.push(foo.textPrint(foo.Utils.serializeTag(node, true)));
				
			}
			
		}
		return result.join('\n');
	},

/*
	Function: parseTokens
		Parse tokens
*/	
	parseTokens: function(){
		var attr = $A(this.element.attributes);
		if(foo.reversion)
			attr = attr.reverse();
		
		for(var i=0; i<attr.length; i++){
			for(var token in this.tokens){
				if(attr[i].nodeName == foo.ns+':'+token){
					var value = attr[i].nodeValue;
					this.element.removeAttribute(attr[i].nodeName);
					this.tokens[token].apply(this, [value]);
				}
			}
		}
	},

/*
	Function: addListener
		Add listener to section event
	
	Parameters:
		name - Event name
		fn - Listener function
*/
	addListener: function(name, fn){
		this.events[name].push(fn);
	},

/*
	Function: removeListener
		Remove listener from section event
	
	Parameters:
		name - Event name
		fn - Listener function
*/
	removeListener: function(name, fn){
		this.events[name] = this.events[name].without(fn);
	},

/*
	Function: fireEvent
		Fire event
	
	Parameters:
		name - Event Name
		[args] - Arguments
*/
	fireEvent: function(){
		var args = $A(arguments), name = args.shift();
		this.events[name] = this.events[name].reject(function(fn){return fn.apply(this, args);}, this);
	},

/*
	Function: putStorage
		Put data to storage
	
	Parameters:
		name - Storage name
		obj - Object to be stored
	
	Returns:
		data id
*/
	putStorage: function(name, obj){
		if(!this.storage[name])
			this.storage[name] = [];
		
		if(this.storage[name].include(obj))
			return this.storage[name].indexOf(obj);
		
		var id = this.storage[name].length;
		this.storage[name].push(obj);
		return id;
	},

/*
	Function: getStorage
		Get data from storage
	
	Parameters:
		name - Storage name
		id - Data id
	
	Returns:
		Data
*/
	getStorage: function(name, id){
		if(this.storage[name])
			return this.storage[name][id];
		else
			return null;
	},

/*
	Function: setElementData
		Store data to ElementStorage that is accessible through DOM. Data can be storage even if the element doesn't exist
	
	Parameters:
		id - Element id
		name - Data name
		data - Data
*/
	setElementData: function(id, name, data){
		if(!this.elementData[id])
			this.elementData[id] = {};
		if(typeof name == 'string')
			this.elementData[id][name] = data;
		else
			Object.extend(this.elementData[id], name);
	},

/*
	Function: removeElementData
		remove data from ElementStorage
	
	Parameters:
		id - Element id
		name - Data name
*/
	removeElementData: function(id, name){
		if(!this.elementData[id])
			delete this.elementData[id][name];
	},

/*
	Function: getElementData
		Get data of element
	
	Parameters:
		id - Element id
		name - Data name
*/
	getElementData: function(id, name){
		if(this.elementData[id])
			return this.elementData[id][name] || null;
	},

/*
	Function: addQueue
		Add function to Queue
	
	Parameters:
		fn - Function
*/
	addQueue: function(fn){
		this.queue.push(fn);
	},

/*
	Function: removeEvent
		Remove event
	
	Parameters:
		event - Event
		fn - Function
*/
	removeEvent: function(event, fn){
		this.events[event] = this.events[event].without(fn);
	},

/*
	Function: register
		Register section/dataset as client
	
	Parameters:
		client - Client
*/
	register: function(client){
		if(!this.clients.include(client)){
			this.clients.push(client);
			client.servers.push(this);
		}
	},

/*
	Function: unregister
		Unegister client
	
	Parameters:
		client - Client
*/
	unregister: function(client){
		this.clients = this.clients.without(client);
		client.servers = client.servers.without(this);
	},

/*
	Function: getAllChildren
		Get all children sections
*/
	getAllChildren: function(){
		return this.children
		.clone()
		.concat(this.children
			.map(function(e){return e.getAllChildren();})
			.flatten()
		);
	},

/*
	Function: rebuildChain
		Rebuild chain
*/
	rebuildChain: function(){
		var children = this.getAllChildren();
		this.addQueue(function(){
			children.each(function(child){
				child.clients.each(function(client){
					$(child.id).section.register(client);
				});
			});
		});
	},

/*
	Function: destroy
		Destroy section
*/
	destroy: function(){ // Preventing memory leaks
		this.servers.invoke('unregister', this);
		this.clients.each(this.unregister.bind(this));
		this.parent = false;
		this.children.invoke('destroy');
		this.children.clear();
		this.fireEvent('destroy');
	},

/*
	Function: toElement
		To element
*/
	toElement: function(){
		return this.element;
	},

/*
	Function: newFuid
		Generate a unique id
*/
	newFuid: function(){
		return this.id + '_' + foo.fuidPrefix + this.unique++;
	},

/*
	Function: getLastFuid
		Get last unique id
*/
	getLastFuid: function(){
		return this.id + '_' + foo.fuidPrefix + (this.unique-1);
	},

/*
	Function: getNextFuid
		Get next unique id
*/
	getNextFuid: function(){
		return this.id + '_' + foo.fuidPrefix + this.unique;
	},

/*
	Function: addFuid
		Give an element an unique id if there is no id exist
*/
	addFuid: function(node){
		if(!node.id){
			node.id = foo.printTag[0]+'this.newFuid()'+foo.printTag[1];
			node.addClassName('fuid');
		}
	}
});



///////Modules
foo.addModule('Base', {standard: true,
	tokens: {
		'implement': function(value){
			value.split(',').each(function(e){
				this.implement(e.strip());
			}, this);
		},
		
		'bind': function(value){
			var self = this;
			this.bindingNames = value.split(',');
			this.bindings = [];
			this.bindingNames.each(function(e, i){
				var id = e.strip();
				if(window[id] && window[id].register){
					window[id].register(self);
					if(i == 0)
						self.binding = window[id];
					else
						self['binding'+i] = window[id];
					
					self.bindings.push(window[id]);
				}
				else{
					foo.addListener('sectionInit', function(section){
						if(window[id] == section && section.register){
							section.register(self);
							if(i == 0)
								self.binding = section;
							else
								self['binding'+i] = section;
							
							self.bindings.push(section);
							self.refresh();
							return true;
						}else
							return false;
					});
				}
			});
		},
		
		'datasource': function(value){
			var args = value.split('#');
			var datasource = window[args.shift()];
			var dspath = args.join('#');
			
			if(datasource){
				this.datasource = datasource;
				this.config.dspath = dspath;
				datasource.register(this);
			}
		},
		
		'config': function(value){
			this.addListener('initialize', function(){
				this.config = Object.extend(this.config, value.evalJSON());
				return true;
			});
		}
	},
	
	statements: {
		'if': function(node, value, options){
			var nextnode = $(node).next();
			if(nextnode && (nextnode.hasAttribute(foo.ns+':else') || nextnode.hasAttribute(foo.ns+':elseif'))){
				while(node.nextSibling != nextnode)
					node.parentNode.removeChild(node.nextSibling);
			}
			return [
				'if('+value+'){',
					this.parse(node),
				'}'
			].join('\n');
		},
		
		'elseif': function(node, value, options){
			var nextnode = $(node).next();
			if(nextnode && (nextnode.hasAttribute(foo.ns+':else') || nextnode.hasAttribute(foo.ns+':elseif'))){
				if(node.nextSibling != nextnode)
					node.parentNode.removeChild(node.nextSibling);
			}
			return [
				'else if('+value+'){',
					this.parse(node),
				'}'
			].join('\n');
		},
		
		'else': function(node, value, options){
			return [
				'else{',
					this.parse(node),
				'}'
			].join('\n');
		},
		
		'each': function(node, source, options){
			var nextnode = $(node).next();
			if(nextnode && (nextnode.hasAttribute(foo.ns+':else') || nextnode.hasAttribute(foo.ns+':elseif'))){
				while(node.nextSibling != nextnode)
					node.parentNode.removeChild(node.nextSibling);
			}
			
			var useIn, useAs, temp, key, value, src;
			key = 'key'+foo.idx;
			src = 'src'+foo.idx;
			value = 'val'+foo.idx++;
			useIn = source.include(' in ');
			useAs = source.include(' as ');
			
			if(useIn){
				temp = source.split(' in ');
				key = temp.first().strip();
				source = temp.last().strip();
			}
			if(useAs){
				temp = source.split(' as ');
				value = temp.last().strip();
				source = temp.first().strip();
			}
			
			var content = this.parse(node)
			
			return [
				'var '+value+', '+key+', '+src+' = '+source+';',
				'if('+src+' && '+src+'.length > 0){',
					'for('+key+'=0;'+key+'<'+src+'.length; '+key+'++){',
						value+' = '+src+'['+key+'];',
						!useIn&&!useAs?'with('+value+'){':'',
							content,
						!useIn&&!useAs?'}':'',
					'}',
				'}else if('+src+' && typeof '+src+'.length != "number"){',
					'for('+key+' in '+src+'){',
						value+' = '+src+'['+key+'];',
						!useIn&&!useAs?'with('+value+'){':'',
							content,
						!useIn&&!useAs?'}':'',
					'}',
				'}',
			].join('\n');
		},
		
		'for': function(node, source, options){
			var nextnode = $(node).next();
			if(nextnode && (nextnode.hasAttribute(foo.ns+':else') || nextnode.hasAttribute(foo.ns+':elseif'))){
				while(node.nextSibling != nextnode)
					node.parentNode.removeChild(node.nextSibling);
			}
			
			var useIn, useAs, temp, key, value, src;
			key = 'key'+foo.idx;
			src = 'src'+foo.idx;
			value = 'val'+foo.idx++;
			useIn = source.include(' in ');
			useAs = source.include(' as ');
			
			if(useIn){
				temp = source.split(' in ');
				key = temp.first().strip();
				source = temp.last().strip();
			}
			if(useAs){
				temp = source.split(' as ');
				value = temp.last().strip();
				source = temp.first().strip();
			}
			
			var strip = node.hasAttribute(foo.ns+':strip');
			node.removeAttribute(foo.ns+':strip');
			
			var content = this.parse(node, {excludeNode:true});
			
			return [
				strip ? '' : foo.textPrint(foo.Utils.serializeTag(node)),
				'var '+value+', '+key+', '+src+' = '+source+';',
				'if('+src+' && '+src+'.length > 0){',
					'for('+key+'=0;'+key+'<'+src+'.length; '+key+'++){',
						value+' = '+src+'['+key+'];',
						!useIn&&!useAs?'with('+value+'){':'',
							content,
						!useIn&&!useAs?'}':'',
					'}',
				'}else if('+src+' && typeof '+src+' != "number"){',
					'for('+key+' in '+src+'){',
						value+' = '+src+'['+key+'];',
						!useIn&&!useAs?'with('+value+'){':'',
							content,
						!useIn&&!useAs?'}':'',
					'}',
				'}',
				strip ? '' : foo.textPrint(foo.Utils.serializeTag(node, true)),
			].join('\n');
		},
		
		'content': function(node, value, options){
			node.innerHTML = foo.printTag[0]+'typeof '+value+' != "undefined" && '+value+' ? '+value+' : '+node.innerHTML.toJSON()+foo.printTag[1];
			return this.parse(node);
		},
		
		'assign': function(node, value, options){
			this.addFuid(node);
			var id = node.hasClassName('fuid') ? 'this.getNextFuid()' : foo.parseInline(node.id);
			return [
				'this.setElementData('+id+', '+value+');',
				this.parse(node)
			].join('\n');
		},
		
		'with': function(node, value, options){
			return [
				'with('+value+'){',
					'try{',
						this.parse(node),
					'}catch(e){}',
				'}'
			].join('\n');
		},
		
		'fuid': function(node, value, options){
			this.addFuid(node);
			var name = eval(foo.parseInline(value));
			var def = !(/\W+/.test(value)) ? 'var ' : '';
			var id = node.hasClassName('fuid') ? 'this.getNextFuid()' : foo.parseInline(node.id);
			return [
				def+name+' = '+id+';',
				this.parse(node)
			].join('\n');
		},
		
		'section-loader': function(node, value, options){
			this.addFuid(node);
			value = '['+eval(foo.parseInline(value))+']';
			var content = node.innerHTML;
			return [
				foo.parseText(foo.Utils.serializeTag(node)),
				foo.textPrint(foo.Utils.serializeTag(node, true)),
				'this.addQueue(function(){',
					'var values = '+value+';',
					'$(values[0]).observe(values[1], function(){',
						'new foo.Section('+(node.hasClassName('fuid') ? 'this.getLastFuid()' : foo.parseInline(node.id))+', '+content.toJSON()+');',
						'$(values[0]).stopObserving(values[1], arguments.callee);',
					'})',
				'});'
			].join('\n')
		},
		
		'script': function(node, value, options){
			var content = node.value || node.innerHTML;
			var scriptText = foo.parseInline(content);
			var script = eval(scriptText);
			if(value == 'inline'){
				return script;
			}else if(value == 'queue'){
				return 'this.addQueue(new Function('+scriptText+'));';
			}else{
				var values = value.split(/\s+/);
				var first = values.shift();
				var second = values.shift();
				if(this['parseAs'+first.substring(0,1).toUpperCase()+first.substring(1)])
					this['parseAs'+first.substring(0,1).toUpperCase()+first.substring(1)](second, content);
			}
		},
		
		'strip': function(node, value, options){
			return this.parse(node, {excludeNode: true});
		}
		
	},
	
	globals: {
		getData: function(){
			if(this.config.dspath.startsWith('xpath:')){
				return []; // not implemented yet
			}else{
				this.data = this.datasource.get();
				if(this.config.dspath){
					try{
						this.data = eval('this.data.'+this.config.dspath);
					}catch(e){
						this.data = false;
					}
				}
			}
		},
		
		parseAsEvent: function(event, content){
			this.addListener(event, new Function('event', content));
		}
	},
	
	listeners: {
		update: function(){
			if(this.datasource)
				this.getData();
		}
	},
	
	initialize: function(){
		
	}
	
	
});